我們先來看以下兩個例子,想想當 <Parent/>
re-render 時,Child
會不會也跟著 re-render 呢?
function Child({ data }) {
return <div>{data.a}</div>;
}
const MemoChild = React.memo(Child);
function Parent() {
const data = { a: 1, b: 2 };
return <MemoChild data={data}/>;
}
function Child({ onClick }) {
return <button onClick={onClick}>click</button>;
}
const MemoChild = React.memo(Child);
function Parent() {
function onClick() {
console.log('hello world!');
}
return <MemoChild onClick={onClick}/>;
}
這兩題的答案都是肯定的,明明都包上了 React.memo
但還是沒用,到底是為什麼呢
讓我們先來看看 React.memo
到底做了哪些事
Docs
React.memo 是一個 HOC,當 props update 的時候會去 invoke 第二個參數 (areEqual[func]) 來判斷要不要進行 re-render。
要注意的是他只有負責監聽 props 的改變,這點和傳統的 shouldComponentUpdate 有些許不同。 另外,預設的 areEqual function 是 shallowCompare
,因此上面兩個例子中,才會被判斷成需要 re-render。
這樣看起來用了 React.memo
反而會增加許多無謂的比較,那到底該不該用 React.memo
呢
Dan Abramov 的這則推文也曾經提到,大多數的 component 理當不會一直收到相同的 props,因此以上題為例,我們應該是要避免發生 Parent 無故 re-render 的行為。
因為 shallow compare 只有 primitive type 會去比較 value(===
),其他都是看 reference 的。
所以useMemo
和 useCallback
就派上用場了。
Docs
回傳一個 memoized 的值。
傳遞一個「建立」function 及依賴 array。useMemo 只會在依賴改變時才重新計算 memoized 的值。這個最佳化可以避免在每次 render 都進行昂貴的計算。
以上題為例:
// const data = { a: 1, b: 2 };
const data = useMemo(() => ({ a: 1, b: 2 }), []);
Docs
回傳一個 memoized 的 callback。
傳遞一個 inline callback 及依賴 array。useCallback 會回傳該 callback 的 memoized 版本,它僅在依賴改變時才會更新。當傳遞 callback 到已經最佳化的 child component 時非常有用,這些 child component 依賴於引用相等性來防止不必要的 render(例如,shouldComponentUpdate)
以上題為例:
/**
* function onClick() {
* console.log('hello world!');
* }
*/
const onClick = useCallback(function() {
console.log('hello world!');
}, []);
如此一來,即使 <Parent/>
re-render,<Child/>
也不會有所影響了。
雖說使用這兩個 hooks 可以解決上述 re-render 的問題,但重點是這值不值得呢?
不光原先的 React.memo 會造成 O(props.length)
的比較,額外為了一個 object literial 來使用 useMemo
究竟划不划算了,我想這就見仁見智了。畢竟也沒有明確的 benchmark 來顯示說該不該使用 useMemo
或 useCallback
來包裝要傳進去的 props。
另外,接下來幾天應該都會分享關於 render 相關的議題,喜歡的可以發摟一下喔~